閱讀本篇文章前,仔細想想看
- 到目前為止對於 TypeScript Interface 介面的理解到什麼程度呢?
- 你認為 TypeScript 和第三方套件/專案/框架協作上會有什麼困難點?(本系列後續文章還會探討更多有關的議題喔!)
如果還沒理解完畢的話,可以先翻看前一篇文章喔!
本日正文前的廢文稍微多一些,請讀者多多諒解~
哇,這已經是第 15 篇了! —— 恩... 本系列文原本打算讓讀者 30 天內速成 TypeScript;不過後來筆者轉換方向,改成以推讀者入坑 TypeScript 為目標(或者不一定用到 TS 但至少從中學到東西),因此才寫作本系列 —— 不過每篇文長也是有些長,不知道有沒有起到效果,寫作過程中也是感到迷茫與不確定的時候。
當初開始寫這篇系列文前,原本沒有想過要入賽。筆者原本是想學 Angular 這個框架,不過後來看到 Angular 是用 TypeScript 作為主流開發語言,心想可能要學習一下 TypeScript。
後來不知道什麼原因,覺得 TypeScript 實在是太有趣,於是爬滿整個文件跟其他 TypeScript 領域的神人們的教學影片與文章,結果不知不覺兩週過去,偶然看到鐵人賽的報名資訊。
然而,看到鐵人賽的報名時,也已經是末尾(差不多剩下一週可以報名),所以才非常緊迫地訂出草稿,並且開始趕文章。打完草稿後丟出來,完蛋 —— 範圍太大啊!啊!啊!啊!啊!啊!超級悲劇!
積極撰文的同時,前十四天的文章也再逐步改善中。(主要是文法部份,本人國文造詣超爛,口語多到炸,連接詞用得很悲劇)前七到八天的文章,筆者重看覺得很丟臉 —— 發文前一天會強迫自己重新審文,每天下午發文一定在按下送出的按鈕前重新看一遍,能夠刪減冗言贅字就刪、換句話說更通順就換。
如果筆者能夠提早看到鐵人賽時間,並且提早準備,應該還可以刪減內容。不過後來想說算了 —— 寫就寫,鐵人賽頂多就這段期間。XDDDD
貼心小提示
要參賽的讀者請不要衝動,三思而後行;但是也可以學筆者這樣 —— 不入虎穴、焉得虎子,搞不好可以教出老虎五隻?
[2019.09.25. AM 00:03 新增訊息]
本系列從 Day 12. 開始到鐵人賽完整 30 天,確定會是《機動藍圖》系列篇章 —— 後續的《戰線擴張》系列會以延長賽方式進行!讀者不要認為作者只講到一半就不寫了~(俗稱射後不理)
本系列作者不是落跑作者啊!!!!!(好歹本系列 30 天後完成趴數有達到 6% 以上!哈!哈!哈!哈!哈!)XDDDDDDD
好了,今天應該也會講得很輕鬆~(希望不要再像昨天一樣,講到後面扯到專案開發上的協作問題,想收尾也難收)
正文開始!
貼心小提示
由於本篇已經是本系列的中間篇章,可能有新的讀者(誤入),因此筆者再進行名詞解釋:
- 狹義物件指得是單純 JSON 物件(
{}
的格式)- 廣義物件指得是包含狹義物件外,還有陣列、函式、類別以及類別建造的物件等等
“恩... 介面不都是只有狹義物件(也就是 JSON 物件)的表現形式嗎?難道其他物件的格式(例如:陣列)也可以被模仿?”
筆者這邊是以個人見解來表示:TypeScript 的介面以及型別都有 —— 針對物件的屬性或索引做更神奇的微調,這個功能稱之為 Indexable Types(中文好難翻:可控索引型別?筆者還是不要亂翻譯好了),它的寫法是這樣:
我們先來看第一個例子,也就是 Dictionary
這個型別。
[propName: string]: T
的意思是 —— 只要屬性為字串型別,其對應的值之型別必須要為 T
。以 Dictionary
的例子來看,這裡的 T
是字串型別 string
;也就是說,任何字串型的索引只能接字串型態的值。
以下舉幾個使用 Dictionary
的狀況(以下程式碼被 TypeScript 檢測結果如圖一)。
圖一:基本上,只要屬性對應的值非字串的話,就會被 TypeScript 警告
由以上結果得知,藉由這種方式可以讓使用者任意新增屬性,但對應的值必須被鎖定在特定的型別(Dictionary
為例,就被鎖在 string
這個型別)。
再來是第二個例子的使用情況 —— StringTypedList
這個介面,但它是使用 [index: number]
—— 也就是屬性為數字型態。(檢測結果如圖二)
圖二:StringTypedList
檢測結果
這邊要特別注意的點有:
string
以外的型態,因此初始化陣列的索引(index)都會以 '0', '1', '2' ...
這種字串的數字形式初始化,所以才會被 TypeScript 判定結果是錯誤!(特別再把錯誤的部分截在圖三中)[index: number]
將索引部分鎖定在 number
型別上,目的是為了防止開發者呼叫字串型別的屬性(或是用點的方式呼叫屬性),而是模擬陣列的行為 -- 用數字來檢索該物件裡的內容
[index: number]
這種狀態下初始化時,必須用 JSON 物件的格式,指定索引的位置(數值)並填入對應的值(當然,值必須符合型別 T
,其中 T
為 [index: number]: T
裡面的 T
)
圖三:儘管屬性專門接收數字型別,但卻不能直接初始化為陣列
另外,StringTypedList
的介面的實作有點類似雜湊表(Hash Table,又被稱為哈希表,直翻感覺有點怪怪)。
重點 1. Indexable Types
若想限制物件索引為特定型別 —— 比如
number
或string
,可以使用以下的格式。其中,Indexable Type 的實作可以在型別系統或介面出現:[keyName: TKey]: TValue
其中,必須遵守下面的規則:
TKey
必須為number
或者是string
其中一種,不能為其他型別與number
和string
的複合格式(連number | string
是不接受的!)TValue
可為任意型別(由於 ES6 有新的
Symbol
型別出現,目前有在討論新增Symbol
作為索引可以接受的型別)
讀者試試看
筆者提供一些沒辦法在短短篇幅內討論的問題,因此這裡給有興趣的讀者去想想:
- 如果知道 ES6
Symbol
這個功能的讀者,有沒有辦法實踐出[keyName: Symbol]: TValue
這種形式?- 這樣的寫法在 TypeScript 是可以被接受的嗎?
type UseBothKeyType = { [key: number]: T1, [key: string]: T2 };
- 這樣的寫法會不會出現問題?
type UserInfo = { name: string, [prop: string]: string; }
- 承上題,那這樣的寫法會不會出現問題?
type UserInfo = { name: string, birth: Date, [prop: string]: string; }
在前幾篇的範例有展示過,這裡稍微提及:
基本上,介面裡運用選用屬性跟型別裡運用的結果都差不多,不過這裡就照搬 Day 13. 探討過後的結果:
至於選用屬性在介面時的行為 —— 就由讀者自行發掘吧~基本上參照 Day 09. 選用屬性 Optional Properties 的文章,在介面時的效果也差沒多少呢。
唯讀(Read-only)屬性標註非常簡單,就是在屬性前加入 readonly
的關鍵字,該屬性就會變成唯讀模式(Read-Only)。另外,這個功能型別系統也可以使用。
以下簡單地測試一下。(TypeScript 檢測結果如圖四;錯誤訊息如圖五)
圖四:硬想寫入含 readoly
的屬性,會被 TypeScript 阻止
圖五:TypeScript 會提醒你,email
屬性是不可以被覆寫的(property 'email' is read-only property
)
重點 2. 唯讀屬性 Read-Only Property
若某型別
T
或介面I
有包含某屬性P
,其中屬性P
前面有標註readonly
:type T = { readonly P: TAny; }; interface I { readonly P: TAny; }
則任何經由型別
T
創造出來或經由I
實踐出來的廣義物件,這些物件的屬性P
只能讀取不能進行覆寫。
英文挺容易讓人誤解 - Hybrid Type 指的不是型別系統裡的一種 type
的表示方式,而是指介面的宣告方式可以用混合的方式呈現。
還記得介面的宣告是哪兩種方式嗎?筆者就聞風不動把 Day 12. 裡的重點搬過來:
《Day 12. 介面宣告 X 使用介面》 之 重點 1. TypeScript 介面(Interface)的定義與種類
TypeScript Interface 的定義方式為,使用關鍵字
interface
而後接介面的詳細定義:
- 物件格式:即 JSON 格式,是為屬性對型別,不是對值
- 單一函式格式:沒有任何屬性,就是函式而已,但不一定需要標上函式名稱
- 混合格式:即將『物件格式』跟『單一函式格式』混合在一起
以下舉計數器 Counter
的介面為範例。
根據以上的程式碼,筆者進行簡單的實作:
將此程式碼進行 TypeScript 的編譯並且使用 node
執行過後如圖六。
圖六:結果順利地按照順序印出 5
、8
、5
這三個結果
儘管混合式的介面可以做出更多不同的物件形式,然而筆者認為這不太是個好方法。
如果忘記要把整個介面的屬性或方法實作出來的話 —— 譬如將 increment
方法拿掉。(TypeScript 檢測結果如圖七)
圖七:結果 TypeScript 沒有理人啊!
編譯過後沒有出錯,但上面的程式碼執行過程一定會出錯。(錯誤結果如圖八)
圖八:counter.increment
根本是未定義的狀態
讀者可能問說,為何這時候 TypeScript 沒辦法檢查這個錯誤呢?
筆者其實也搞不清楚這裡的註記與推論機制到底是什麼。之所以提及這個部分的主要目的是為了展示給讀者:“你可以使用介面的混合型態,不過使用起來可能會遇到這種問題 —— 忘記實踐出混合型態介面裡的屬性與方法”。
筆者目前也想不太到什麼情況下會用介面的混合型態(Hybrid Type Interface),因此沒花太多時間研究這個雷點。
混合型態的用法在官方有說明,但筆者仍然認為:“TypeScript Class 和 Interface,光是學好基本的 OOP 與設計模式就夠實用了!”
所以呢,這裡筆者就不下重點了~讀者自己瞧吧!呵!呵!呵!呵!呵!(有病)
本日篇章主要是把介面的雜項部分補齊。讀者應該會發現,除了混合型態的介面以外,其他的 Feature 都可以在 type
與 interface
使用,這會讓學 TypeScript 的初心者們覺得:介面跟型別系統好像都沒差的。
下一篇就是要統整型別系統跟介面的比較!因為筆者基本上已經把該介紹的介面相關語法都介紹完了。
另外,讀者有沒有發現一件事情?從本系列開講到目前為止 —— 執行 TypeScript Compiler 的次數:只有 3 次!
使用普通 JS 的狀況,大部分得執行過程式碼,才能根據 Error Stack 結果除 Bug。
相比起來,TypeScript 的好處就是:
“不需要經過編譯,TypeScript 編譯器就會靜態地提醒程式碼哪裡會有潛在的 Bug”
這裡當然不是指 TypeScript 一定比原生 JS 好,反倒是先把原生 JS 基礎熟練過後,學習 TypeScript 才會如魚得水。(筆者回想起學 JS 的經驗,過程也是蠻莫名其妙的 XD,有空再說)
回過頭來,想要讓上述 TypeScript 的優點發威也是需要使用者非常清楚哪些事情是在 TypeScript 可以做的。
只要非常~非常~清楚 TypeScript 的推論與註記機制基本上就已經免除六到八成的 Bug,其餘就是學習更多語法與應用,剩下兩成可能就是一些開發者需要注意的或真的是稀有的案例,連筆者腦袋也想像不到的狀況呢~
筆者認定:後面的文章講再好,《前線維護》系列篇章是入門 TypeScript 基礎中的基礎!
我覺得口語很好呀XD很好懂
最近剛好也在研究ts...覺得博大精深...
感謝支持 XD
不過筆者才剛碰型別系統也不到兩三個月時間,也深感覺得 TS 真的是TNND大坑
請問圖三的部份是不是有筆誤呢?
interface 定義的值只能接收 string 型別
interface StringTypedList {
[index: number]: string;
}
但圖三的範例,宣告時給了 number 會報錯應該是正常的吧?
let stringTypedArrayLiteral: StringTypedList = [1, 2, 3];
這邊宣告我嘗試給予了 string 沒有收到 TS 的報錯 XD
let stringTypedArrayLiteral: StringTypedList = ['a', 'b', 'c'];
是不是我有搞錯什麼了 XD
我猜是筆者搞錯了,雖然官方文件那頁已經改了,但是舊的文件中,也是可以直接用 Array 。
文章中提到Indexable Types必須遵守的規則的其中一點:「TKey 必須為 number 或者是 string 其中一種,不能為其他型別與 number 和 string 的複合格式(連 number | string 是不接受的!)」
但我經過測試後, 得到的結果卻是可以使用 number | string 的複合格式, 我的TypeScript的版本是4.5.5, 測試結果如圖所示
圖七 沒有報錯,應該是 as
的特性
// EX. 這樣也不會報錯
const a = { x: 1 } as { x: number; y: string }
但目前我還想不出 Hybrid Types Interface 直接註記的寫法(也可能不行)